package com.liaoyifan.core.util;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.liaoyifan.core.constant.Constants;
import com.liaoyifan.core.enums.AuthCodeEnum;
import com.liaoyifan.core.exception.AuthException;
import com.liaoyifan.core.model.AuthClaims;
import com.liaoyifan.core.model.AuthToken;
import com.liaoyifan.core.valid.Assert;
import com.liaoyifan.core.valid.Lang;
import io.jsonwebtoken.ClaimJwtException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.JwtParserBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Deserializer;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.jackson.io.JacksonDeserializer;
import io.jsonwebtoken.jackson.io.JacksonSerializer;
import jakarta.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.crypto.SecretKey;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.redisson.api.RList;
import org.redisson.api.RMap;
import org.redisson.api.RSet;

/**
 * JWT令牌工具类，负责访问令牌(Access Token)和刷新令牌(Refresh Token)的全生命周期管理， 包括令牌生成、解析、验证、吊销、黑名单管理等功能。
 * 令牌信息通过Redis存储，支持分布式环境下的令牌状态同步，确保多实例部署时的一致性。
 */
@SuppressWarnings("unused")
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Log4j2
public class Token {

    /** 令牌类型枚举：区分访问令牌和刷新令牌 */
    public enum TokenType {
        ACCESS, // 访问令牌：短期有效，用于接口访问授权
        REFRESH // 刷新令牌：长期有效，用于获取新的访问令牌
    }

    // ========================= 常量配置 =========================

    /** JWT签发者标识 */
    private static final String ISSUER = "liaoyifan";

    /** 访问令牌过期时间：1小时 */
    private static final Duration ACCESS_EXPIRE = Duration.ofHours(1);

    /** 刷新令牌过期时间：30天 */
    private static final Duration REFRESH_EXPIRE = Duration.ofDays(30);

    /** 过期缓冲时间：10秒（避免时钟偏差导致的令牌提前失效） */
    private static final Duration EXPIRY_BUFFER = Duration.ofSeconds(10);

    /** 访问令牌签名密钥 */
    private static final SecretKey ACCESS_KEY =
            Secret.getMacKey("X3WwQzpg5hvKZ4plQIxUYMH88HnjYELDhYXfiL2MGGI=");

    /** 刷新令牌签名密钥（与访问令牌密钥分离，提高安全性） */
    private static final SecretKey REFRESH_KEY =
            Secret.getMacKey("wb3HHEPGh8EROHu6hcX6TMwnVLBqKSkUK1HnZScrvdI=");

    /** Redis键前缀：用于区分令牌相关的缓存键 */
    private static final String TOKEN_PREFIX = "token:";

    /** 黑名单键前缀：存储已吊销的令牌ID */
    private static final String BLACKLIST_PREFIX = TOKEN_PREFIX + "blacklist:";

    /** Redis键前缀：用于区分令牌相关的缓存键 */
    private static final String PERMISSIONS_PREFIX = TOKEN_PREFIX + "permissions:";

    // ========================= 序列化配置 =========================

    /** JWT声明序列化器（基于Jackson） */
    private static final Serializer<Map<String, ?>> SERIALIZER;

    /** JWT声明反序列化器（基于Jackson） */
    private static final Deserializer<Map<String, ?>> DESERIALIZER;

    static {
        ObjectMapper mapper = Json.OBJECT_MAPPER;
        SERIALIZER = new JacksonSerializer<>(mapper);
        DESERIALIZER = new JacksonDeserializer<>(mapper);
    }

    // ========================= JWT构建与解析 =========================

    /**
     * 创建JWT构建器（基础配置）
     *
     * @param jti 令牌唯一ID（用于标识令牌，建议使用UUID）
     * @param subject 令牌主题（通常为用户ID或唯一标识）
     * @param key 签名密钥
     * @param expire 过期时间
     * @return JwtBuilder实例
     */
    private static JwtBuilder createBuilder(
            String jti, String subject, SecretKey key, Duration expire) {
        Assert.notBlank(jti, "jti cannot be blank");
        Assert.notBlank(subject, "subject cannot be blank");
        Assert.notNull(key, "key cannot be null");
        Assert.notNull(expire, "expire cannot be null");
        Instant now = Instant.now();
        return Jwts.builder()
                .json(SERIALIZER) // 设置序列化器
                .id(jti) // 令牌ID
                .subject(subject) // 主题
                .issuer(ISSUER) // 签发者
                .issuedAt(Date.from(now)) // 签发时间
                .expiration(Date.from(now.plus(expire))) // 过期时间
                .signWith(key); // 签名密钥
    }

    /**
     * 创建JWT解析器（基础配置）
     *
     * @param key 签名验证密钥（需与构建时使用的密钥一致）
     * @return JwtParserBuilder实例
     */
    private static JwtParserBuilder createParser(SecretKey key) {
        Assert.notNull(key, "key cannot be null");
        return Jwts.parser()
                .json(DESERIALIZER) // 设置反序列化器
                .verifyWith(key) // 验证签名的密钥
                .requireIssuer(ISSUER); // 校验签发者必须为预设值
    }

    // ========================= Redis键生成 =========================

    /**
     * 生成令牌在Redis中的存储键（用于存储令牌详情）
     *
     * @param type 令牌类型（ACCESS/REFRESH）
     * @param jti 令牌ID（jti）
     * @return Redis键名
     */
    private static String getTokenKey(TokenType type, String jti) {
        Assert.notBlank(jti, "jti cannot be blank");
        Assert.notNull(type, "type cannot be null");
        return TOKEN_PREFIX + type.name().toLowerCase() + ":" + jti;
    }

    /**
     * 生成主题关联的令牌集合键（用于存储用户的所有令牌ID）
     *
     * @param type 令牌类型（ACCESS/REFRESH）
     * @param subject 主题（用户标识）
     * @return Redis键名
     */
    private static String getSubjectKey(TokenType type, String subject) {
        Assert.notBlank(subject, "subject cannot be blank");
        Assert.notNull(type, "type cannot be null");
        return TOKEN_PREFIX + "subject:" + subject + ":" + type.name().toLowerCase();
    }

    // ========================= 令牌生成 =========================

    /**
     * 生成令牌（通用方法，支持访问令牌和刷新令牌）
     *
     * @param jti 令牌ID（建议使用UUID）
     * @param subject 主题（用户标识）
     * @param type 令牌类型
     * @param claims 自定义声明（可选，如用户角色、权限等）
     * @return 生成的JWT令牌字符串
     */
    private static String createToken(
            String jti, String subject, TokenType type, Map<String, ?> claims) {
        Assert.notNull(type, "type cannot be null");
        // 根据令牌类型选择密钥和过期时间
        SecretKey key = type == TokenType.ACCESS ? ACCESS_KEY : REFRESH_KEY;
        Duration expire = type == TokenType.ACCESS ? ACCESS_EXPIRE : REFRESH_EXPIRE;
        // 构建JWT
        JwtBuilder builder = createBuilder(jti, subject, key, expire);
        // 添加自定义声明（如果有）
        if (claims != null && !claims.isEmpty()) {
            builder.claims().add(claims).and();
        }
        String token = builder.compact();

        // 存储令牌信息到Redis（用于验证和吊销）
        // 1. 存储用户与令牌ID的关联（便于批量吊销用户的所有令牌）
        RSet<String> jtiSet = Redis.getSet(getSubjectKey(type, subject));
        jtiSet.add(jti);
        jtiSet.expire(expire.plus(EXPIRY_BUFFER)); // 过期时间略长于令牌本身，避免提前删除

        // 2. 存储令牌ID与主题的关联（用于验证令牌有效性）
        RMap<String, String> tokenMap = Redis.getMap(getTokenKey(type, jti));
        tokenMap.put("subject", subject);
        tokenMap.expire(expire.plus(EXPIRY_BUFFER));

        log.info("生成{}令牌成功，jti={}, subject={}", type, jti, subject);
        return token;
    }

    /**
     * 生成访问令牌
     *
     * @param jti 令牌ID（建议使用UUID）
     * @param subject 主题（用户标识）
     * @param claims 自定义声明（如用户角色、权限等）
     * @return 访问令牌字符串
     */
    public static String createAccess(String jti, String subject, Map<String, ?> claims) {
        return createToken(jti, subject, TokenType.ACCESS, claims);
    }

    /**
     * 生成刷新令牌（无自定义声明，仅用于刷新访问令牌）
     *
     * @param jti 令牌ID（建议使用UUID）
     * @param subject 主题（用户标识）
     * @return 刷新令牌字符串
     */
    public static String createRefresh(String jti, String subject) {
        return createToken(jti, subject, TokenType.REFRESH, null);
    }

    // ========================= 令牌吊销与黑名单 =========================

    /**
     * 将令牌ID加入黑名单（标记为已吊销）
     *
     * @param jti 令牌ID
     * @param expire 黑名单过期时间（秒），建议设置为令牌剩余有效期
     */
    public static void addBlacklist(String jti, long expire) {
        Assert.notBlank(jti, "jti cannot be blank");
        RMap<String, String> blacklistMap = Redis.getMap(BLACKLIST_PREFIX + jti);
        blacklistMap.put("revoked", Constants.TRUE);
        blacklistMap.expire(Duration.ofSeconds(expire).plus(EXPIRY_BUFFER));
        log.info("令牌jti={}已加入黑名单，过期时间={}秒", jti, expire);
    }

    /**
     * 吊销指定令牌（从Redis中删除并加入黑名单）
     *
     * @param jti 令牌ID
     * @param type 令牌类型
     */
    public static void removeToken(String jti, TokenType type) {
        String tokenKey = getTokenKey(type, jti);
        RMap<String, String> tokenMap = Redis.getMap(tokenKey);

        // 从用户-令牌关联集中移除该令牌ID
        String subject = tokenMap.get("subject");
        if (subject != null) {
            String subjectKey = getSubjectKey(type, subject);
            Redis.getSet(subjectKey).remove(jti);
            log.info("从用户{}的{}令牌集中移除jti={}", subject, type, jti);
        }

        // 删除令牌详情缓存
        long ttl = tokenMap.remainTimeToLive() / 1000; // 剩余有效期（秒）
        tokenMap.delete();
        // 加入黑名单（过期时间为剩余有效期）
        addBlacklist(jti, ttl);
        log.info("吊销{}令牌jti={}成功", type, jti);
    }

    /**
     * 吊销访问令牌
     *
     * @param jti 访问令牌ID
     */
    public static void removeAccess(String jti) {
        removeToken(jti, TokenType.ACCESS);
    }

    /**
     * 吊销刷新令牌
     *
     * @param jti 刷新令牌ID
     */
    public static void removeRefresh(String jti) {
        removeToken(jti, TokenType.REFRESH);
    }

    /**
     * 吊销指定用户的所有令牌（包括访问令牌和刷新令牌）(修改密码，注销用户场景)
     *
     * @param subject 用户标识（主题）
     */
    public static void removeSubject(String subject) {
        removeSubjectTokens(subject, TokenType.ACCESS);
        removeSubjectTokens(subject, TokenType.REFRESH);
        removePermissions(subject);
        log.info("用户{}的所有令牌已吊销", subject);
    }

    /**
     * 吊销指定用户的某类令牌（内部实现）
     *
     * @param subject 用户标识
     * @param type 令牌类型
     */
    private static void removeSubjectTokens(String subject, TokenType type) {
        String subjectKey = getSubjectKey(type, subject);
        RSet<String> jtiSet = Redis.getSet(subjectKey);

        // 遍历并吊销该用户的所有令牌ID
        jtiSet.readAll().forEach(jti -> removeToken(jti, type));
        // 删除用户-令牌关联集
        jtiSet.delete();
    }

    // ========================= 令牌验证 =========================

    /**
     * 检查令牌是否在黑名单中
     *
     * @param jti 令牌ID
     * @return true：已拉黑；false：未拉黑
     */
    private static boolean isBlacklisted(String jti) {
        Assert.notBlank(jti, "jti cannot be blank");
        return Redis.getMap(BLACKLIST_PREFIX + jti).isExists();
    }

    /**
     * 验证令牌有效性（检查Redis缓存和黑名单）
     *
     * @param jti 令牌ID
     * @param type 令牌类型
     * @return true：有效；false：无效（已过期、已吊销或不存在）
     */
    private static boolean isValidToken(String jti, TokenType type) {
        Assert.notBlank(jti, "jti cannot be blank");
        // 1. 检查是否在黑名单
        if (isBlacklisted(jti)) {
            log.info("{}令牌jti={}已在黑名单中，验证失败", type, jti);
            return false;
        }

        // 2. 检查Redis缓存是否存在
        RMap<String, String> tokenMap = Redis.getMap(getTokenKey(type, jti));
        if (!tokenMap.isExists()) {
            log.info("{}令牌jti={}不存在于缓存中，验证失败", type, jti);
            return false;
        }

        // 3. 检查是否过期（预留缓冲时间）
        long ttl = tokenMap.remainTimeToLive();
        boolean valid = ttl > EXPIRY_BUFFER.toMillis();
        if (!valid) {
            log.info("{}令牌jti={}已过期（剩余TTL={}ms），验证失败", type, jti, ttl);
        }
        return valid;
    }

    /**
     * 验证访问令牌有效性
     *
     * @param jti 访问令牌ID
     * @return true：有效；false：无效
     */
    public static boolean validAccess(String jti) {
        return isValidToken(jti, TokenType.ACCESS);
    }

    /**
     * 验证刷新令牌有效性
     *
     * @param jti 刷新令牌ID
     * @return true：有效；false：无效
     */
    public static boolean validRefresh(String jti) {
        return isValidToken(jti, TokenType.REFRESH);
    }

    // ========================= 令牌解析 =========================

    /**
     * 解析JWT令牌中的声明（Claims）
     *
     * @param key 签名密钥（需与生成时一致）
     * @param token JWT令牌字符串
     * @return 解析后的Claims对象
     */
    public static Claims parseClaims(SecretKey key, String token) {
        Assert.notBlank(token, "token cannot be blank");
        return createParser(key).build().parseSignedClaims(token).getPayload();
    }

    /**
     * 解析JWT访问令牌中的声明（Claims）
     *
     * @param token JWT令牌字符串
     * @return 解析后的Claims对象
     */
    public static Claims parseAccess(String token) {
        try {
            return parseClaims(ACCESS_KEY, token);
        } catch (JwtException e) {
            throw new AuthException(AuthCodeEnum.ACCESS_TOKEN_INVALID, e);
        }
    }

    /**
     * 解析JWT刷新令牌中的声明（Claims）
     *
     * @param token JWT令牌字符串
     * @return 解析后的Claims对象
     */
    public static Claims parseRefresh(String token) {
        try {
            return parseClaims(REFRESH_KEY, token);
        } catch (JwtException e) {
            throw new AuthException(AuthCodeEnum.REFRESH_TOKEN_INVALID, e);
        }
    }

    /**
     * 获取当前请求的访问令牌声明（依赖RequestContext）
     *
     * @return 解析后的Claims对象
     */
    public static Claims currentAccess() {
        return currentAccess(RequestContext.getRequest());
    }

    /**
     * 获取当前请求的访问令牌声明
     *
     * @return 解析后的Claims对象
     */
    public static Claims currentAccess(HttpServletRequest request) {
        String token = getAuthorization(request);
        if (token == null || token.isBlank()) {
            throw new AuthException(AuthCodeEnum.HEADER_MISSING_AUTHENTICATION);
        }
        return parseAccess(token);
    }

    /**
     * 获取当前请求的令牌主题（用户标识）
     *
     * @return 主题字符串（用户标识）；获取失败时返回Constants.GUEST（访客）
     */
    public static String subject() {
        return subject(RequestContext.getRequest());
    }

    /**
     * 获取当前请求的令牌主题（用户标识）
     *
     * @return 主题字符串（用户标识）；获取失败时返回Constants.GUEST（访客）
     */
    public static String subject(HttpServletRequest request) {
        try {
            return currentAccess(request).getSubject();
        } catch (Exception e) {
            return Constants.GUEST;
        }
    }

    /** 获取权限存储键 */
    public static String getPermissionsKey(String subject) {
        return PERMISSIONS_PREFIX + subject;
    }

    /** 设置权限 */
    public static List<String> setPermissions(String subject, List<String> permissions) {
        if (Lang.isBlank(subject) || Lang.isEmpty(permissions)) {
            return null;
        }
        String permissionsKey = getPermissionsKey(subject);
        Redis.delete(permissionsKey);
        RList<String> permissionsList = Redis.getList(permissionsKey);
        permissionsList.addAll(permissions);
        permissionsList.expire(REFRESH_EXPIRE.plus(EXPIRY_BUFFER));
        return permissions;
    }

    /** 删除权限 */
    public static void removePermissions(String subject) {
        Redis.delete(getPermissionsKey(subject));
    }

    /** 获取权限 */
    public static List<String> getPermissions(String subject) {
        RList<String> permissionsList = Redis.getList(getPermissionsKey(subject));
        return permissionsList.readAll();
    }

    /** 判断是否有权限 */
    public static boolean hasPermission(String subject, String servletPath) {
        if (Lang.isEmpty(subject)) {
            return false;
        }
        List<String> permissions = getPermissions(subject);
        if (Lang.isEmpty(permissions)) {
            return false;
        }
        return permissions.stream()
                .anyMatch(permission -> Constants.PATH_MATCHER.match(permission, servletPath));
    }

    /** 获取当前请求的访问令牌 */
    public static String getAuthorization(HttpServletRequest request) {
        if (request == null) {
            return null;
        }
        String header = request.getHeader(RequestContext.AUTHORIZATION);
        if (header == null || header.isBlank() || !header.startsWith(RequestContext.BEARER)) {
            return null;
        }
        String authorization = header.substring(RequestContext.BEARER.length());
        return authorization.isBlank() ? null : authorization;
    }

    /** 创建授权令牌(用户登录) */
    public static AuthToken createAuthToken(String subject, Supplier<AuthClaims> supplier) {
        AuthToken i = new AuthToken();
        AuthClaims scope = supplier.get();
        i.setAccessToken(createAccess(UID.get(), subject, scope.getClaims()));
        i.setRefreshToken(createRefresh(UID.get(), subject));
        i.setExpiresIn(ACCESS_EXPIRE.toSeconds());
        i.setScope(setPermissions(subject, scope.getPermissions()));
        return i;
    }

    /** 刷新授权令牌(过期刷新) */
    public static AuthToken refreshAuthToken(
            String refreshToken, Function<String, AuthClaims> func) {
        String subject = removeAuthToken(refreshToken);
        return createAuthToken(subject, () -> func.apply(subject));
    }

    /** 删除授权令牌(退出登录) */
    public static String removeAuthToken(String refreshToken) {
        Claims refresh = parseRefresh(refreshToken);
        String refreshId = refresh.getId(), subject = refresh.getSubject();
        if (!validRefresh(refreshId)) {
            throw new AuthException(AuthCodeEnum.REFRESH_TOKEN_INVALID);
        }
        String token = getAuthorization(RequestContext.getRequest());
        if (token == null || token.isBlank()) {
            throw new AuthException(AuthCodeEnum.HEADER_MISSING_AUTHENTICATION);
        }
        Claims access;
        try {
            access = parseClaims(ACCESS_KEY, token);
        } catch (ClaimJwtException e) {
            access = e.getClaims();
        }
        if (access != null
                && Lang.equals(access.getSubject(), subject)
                && validAccess(access.getId())) {
            removeAccess(access.getId());
        }
        removeRefresh(refreshId);
        return subject;
    }
}
